uiautomation
2025/10/20
uiautomation
- https://crates.io/crates/uiautomation
 - https://github.com/leexgone/uiautomation-rs
 - 需求、场景、同类: 见 ../Tauri光标位置展开面板
 
Feature 特征
| Feature | 描述 | 默认值 | 
|---|---|---|
process | 支持进程操作并按进程ID筛选 | False | 
dialog | 启用消息框显示信息 | False | 
input | 支持键盘输入 | True | 
clipboard | 支持剪贴板操作 | False | 
pattern | 支持微软UI自动化控件模式 | - | 
control | 支持将UI元素封装为控件以简化操作 | True | 
event | 支持微软UI自动化事件 | False | 
log | 使用日志库输出调试信息 | False | 
all | 启用以上所有功能 | False | 
demo
demo —— 打印所有 UI 元素
官方 github demo
(打印内容超多超久,尝试前可以限制一下)
use uiautomation::Result; // 不建议 use 这个,这个Result很容易和标准Result冲突导致报错
use uiautomation::UIAutomation;
use uiautomation::UIElement;
use uiautomation::UITreeWalker;
fn main() {
    let automation = UIAutomation::new().unwrap();
    let walker = automation.get_control_view_walker().unwrap();
    let root = automation.get_root_element().unwrap();
    print_element(&walker, &root, 0).unwrap();
}
fn print_element(walker: &UITreeWalker, element: &UIElement, level: usize) -> Result<()> {
    for _ in 0..level {
        print!(" ")
    }
    println!("{} - {}", element.get_classname()?, element.get_name()?);
    if let Ok(child) = walker.get_first_child(&element) {
        print_element(walker, &child, level + 1)?;
        let mut next = child;
        while let Ok(sibling) = walker.get_next_sibling(&next) {
            print_element(walker, &sibling, level + 1)?;
            next = sibling;
        }
    }
    
    Ok(())
}demo —— 打开笔记本并输入文本
官方 github demo
use uiautomation::core::UIAutomation;
use uiautomation::processes::Process;
fn main() {
    Process::create("notepad.exe").unwrap();
    let automation = UIAutomation::new().unwrap();
    let root = automation.get_root_element().unwrap();
    let matcher = automation.create_matcher().from(root).timeout(10000).classname("Notepad");
    if let Ok(notepad) = matcher.find_first() {
        println!("Found: {} - {}", notepad.get_name().unwrap(), notepad.get_classname().unwrap());
        notepad.send_keys("Hello,Rust UIAutomation!{enter}", 10).unwrap();
        let window: WindowControl = notepad.try_into().unwrap();
        window.maximize().unwrap();
    }
}demo —— 获得属性 as Variant
use uiautomation::UIAutomation;
use uiautomation::types::UIProperty;
use uiautomation::variants::Variant;
fn main() {
    let automation = UIAutomation::new().unwrap();
    let root = automation.get_root_element().unwrap();
    let name: Variant = root.get_property_value(UIProperty::Name).unwrap();
    println!("name = {}", name.get_string().unwrap());
    let ctrl_type: Variant = root.get_property_value(UIProperty::ControlType).unwrap();
    let ctrl_type_id: i32 = ctrl_type.try_into().unwrap();
    println!("control type = {}", ctrl_type_id);
    let enabled: Variant = root.get_property_value(UIProperty::IsEnabled).unwrap();
    let enabled_str: String = enabled.try_into().unwrap();
    println!("enabled = {}", enabled_str);
}demo —— 模拟键盘输入
use uiautomation::core::UIAutomation;
fn main() {
    let automation = UIAutomation::new().unwrap();
    let root = automation.get_root_element().unwrap();
    root.send_keys("{Win}D", 10).unwrap();
}demo —— 添加事件处理程序
struct MyFocusChangedEventHandler{}
impl CustomFocusChangedEventHandler for MyFocusChangedEventHandler {
    fn handle(&self, sender: &uiautomation::UIElement) -> uiautomation::Result<()> {
        println!("Focus changed: {}", sender);
        Ok(())
    }
}
fn main() {
    let note_proc = Process::create("notepad.exe").unwrap();
    let automation = UIAutomation::new().unwrap();
    let root = automation.get_root_element().unwrap();
    let matcher = automation.create_matcher().from(root).timeout(10000).classname("Notepad");
    if let Ok(notepad) = matcher.find_first() {
        let focus_changed_handler = MyFocusChangedEventHandler {};
        let focus_changed_handler = UIFocusChangedEventHandler::from(focus_changed_handler);
        automation.add_focus_changed_event_handler(None, &focus_changed_handler).unwrap();
        let text_changed_handler: Box<CustomPropertyChangedEventHandlerFn> = Box::new(|sender, property, value| {
            println!("Property changed: {}.{:?} = {}", sender, property, value);
            Ok(())
        });
        let text_changed_handler = UIPropertyChangedEventHandler::from(text_changed_handler);
        automation.add_property_changed_event_handler(¬epad, uiautomation::types::TreeScope::Subtree, None, &text_changed_handler, &[UIProperty::ValueValue]).unwrap();
    }
    println!("waiting for notepad.exe...");
    note_proc.wait().unwrap();
}uiautomation与Tauri一同使用
同一线程COM冲突问题,COM library not initialized
解决方法: 不要在同一线程中同时使用 uia 和 tauri,前者可以另开一个线程去用
use tauri::{
    menu::{Menu, MenuItem},
    tray::TrayIconBuilder,
    Manager,
};
use uiautomation::{
    // Result, // 这行代码告诉编译器:“在这个函数里,当我写 Result 的时候,我指的不是标准库里的 std::result::Result,而是 uiautomation 这个库里定义的 Result
    // Result 最好不要use,容易出报错
    UIAutomation,
    UIElement,
    UITreeWalker,
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    // uia
    thread::spawn(|| {
        // 在新线程中初始化 uiautomation。否则会报错 "COM library not initialized"
        // 原因: 不要让同一线程中同时使用uia和tauri,他们会都尝试去初始化COM
        let automation = UIAutomation::new().unwrap();
        let walker = automation.get_control_view_walker().unwrap();
        let root = automation.get_root_element().unwrap();
        print_element(&walker, &root, 0).unwrap();
    });
    // 日志插件
    let log_plugin = tauri_plugin_log::Builder::new()
        .level(log::LevelFilter::Debug) // 日志级别
        .clear_targets()
        // 打印到终端
        .target(tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout))
        // 打印到前端控制台 (前端要开下attachConsole)
        // .target(tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview))
        // 打印到日志文件
        // .target(tauri_plugin_log::Target::new(
        //     tauri_plugin_log::TargetKind::Folder {
        //         path: std::path::PathBuf::from("/path/to/logs"), // 会相对于根盘符的绝对路径
        //         file_name: None,
        //     },
        // ))
        .build();
    tauri::Builder::default()
        .plugin(log_plugin)
        .plugin(tauri_plugin_global_shortcut::Builder::new().build()) // 全局快捷键插件
        .plugin(tauri_plugin_opener::init())
        .setup(|app| {
            let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; // 退出菜单项
            let config_item = MenuItem::with_id(app, "config", "Config", true, None::<&str>)?; // 新增配置菜单项
            let menu = Menu::with_items(app, &[&quit_item, &config_item])?; // 菜单项数组
            let _tray = TrayIconBuilder::new()
                .icon(app.default_window_icon().unwrap().clone()) // 托盘图标
                .tooltip("any-menu")
                .menu(&menu) // 加载菜单项数组
                // .show_menu_on_left_click(true) // 左键也能展开菜单
                .on_menu_event(|app, event| match event.id.as_ref() {
                    // 菜单事件
                    "quit" => {
                        app.exit(0);
                    }
                    // 打开配置窗口
                    "config" => {
                        // 如果配置窗口已存在,直接显示并聚焦
                        if let Some(window) = app.get_webview_window("config") {
                            let _ = window.show();
                            let _ = window.set_focus();
                        }
                        // 如果配置窗口不存在,创建新窗口
                        else {
                            let _config_window = tauri::WebviewWindowBuilder::new(
                                app,
                                "config",
                                tauri::WebviewUrl::App("config.html".into()), // 或者你的配置页面路径
                            )
                            .title("AnyMenu - Config")
                            .inner_size(600.0, 500.0)
                            .min_inner_size(400.0, 300.0)
                            .center()
                            .resizable(true)
                            .build();
                        }
                    }
                    _ => {}
                })
                // .on_tray_icon_event(|tray, event| {
                //     if let TrayIconEvent::Click {
                //         button: MouseButton::Left,
                //         button_state: _,
                //         ..
                //     } = event
                //     {
                //         let app = tray.app_handle();
                //         if let Some(window) = app.get_webview_window("main") {
                //             let _ = window.show();
                //             let _ = window.set_focus();
                //         }
                //     }
                // })
                .build(app)?;
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![greet, paste, send, read_file, get_caret_xy])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
fn print_element(walker: &UITreeWalker, element: &UIElement, level: usize) -> uiautomation::Result<()> {}调用/线程通信问题
前面说了uiautomation不能和tauri在同一线程下工作
那么我们如何在Tauri中调用uiautomation呢?以下提供示例:在 get_caret_xy 中去调用 print_element 函数
这里尽量少修改,修改了三处:
- 主Tauri线程中初始化通道,并将通道对象,放入uia线程和Tauri线程
 - uia线程中启用循环监听事件
 - 函数中使用注入的uia_sender来发送事件,以通知uia线程调用方法
 
se std::sync::mpsc::{self, Sender, Receiver};
use std::sync::{Arc, Mutex};
use tauri::State;
use uiautomation::{
    // Result, // 这行代码告诉编译器:“在这个函数里,当我写 Result 的时候,我指的不是标准库里的 std::result::Result,而是 uiautomation 这个库里定义的 Result
    // Result 最好不要use,容易出报错
    UIAutomation,
    UIElement,
    UITreeWalker,
};
// #region uia thread
// 定义全局 Sender 类型
struct UiaSender(pub Mutex<Sender<UiaMsg>>);
// 消息枚举,根据需求可扩展
enum UiaMsg {
    PrintElement,
}
fn start_uia_worker(rx: Receiver<UiaMsg>) {
    thread::spawn(move || {
        // 初始化 uiautomation
        let automation = UIAutomation::new().unwrap();
        let walker = automation.get_control_view_walker().unwrap();
        let root = automation.get_root_element().unwrap();
        loop {
            match rx.recv() {
                Ok(UiaMsg::PrintElement) => {
                    let _ = print_focused_element(&walker, &automation, 0);
                }
                Err(_) => break,
            }
        }
    });
}
// #endregion
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    // uia
    // 新增:初始化channel
    let (tx, rx) = mpsc::channel::<UiaMsg>();
    let uia_sender = UiaSender(Mutex::new(tx));
    // 启动worker线程,传递receiver
    start_uia_worker(rx);
    
    ...
    
    tauri::Builder::default()
        .manage(uia_sender) // 依赖注入,注入到Tauri State管理
    
    ...
}
#[tauri::command]
fn get_caret_xy(app_handle: tauri::AppHandle, uia_sender: State<UiaSender>) -> (i32, i32) {
    let mut x = 0;
    let mut y = 0;
    // uia
    // 向worker线程发消息
    let tx = uia_sender.0.lock().unwrap();
    let _ = tx.send(UiaMsg::PrintElement);
    return print_msg(app_handle);
    // return (x, y);
}链接到当前文件 1